<template>
  <div id="vlist">
    <div ref="containRef" class="container" @scroll="scrollEvent">
      <div class="phantom" :style="{ height: phantomHeight + 'px' }"></div>
      <div class="content" :style="{ transform: getTransform }">
        <div v-for="item in visibleData" ref="items" :key="item.id" class="content-item"
          :style="{ height: itemHeight + 'px', lineHeight: itemHeight + 'px' }">
          {{ item.id }}
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>

import { computed, onMounted, reactive, ref } from 'vue';
const listData = ref(new Array(100).fill({}).map((item, index) => ({ id: index })))
const itemHeight = ref(60)
const visibleInfo = reactive({
  startIndex: 0, // 开始索引
  endIndex: 0, // 结束索引
  height: 0, // 可视区域高度
})
const bufferCount = ref(3)
const aboveCount = computed(() => { // 缓冲加载个数
  return visibleInfo.startIndex - bufferCount.value > 0 ? 3 : visibleInfo.startIndex
})
const belowCount = computed(() => { // 缓冲加载个数
  return visibleInfo.endIndex + bufferCount.value > itemHeight.length ? 0 : 3
})
const visibleData = computed(() => {  // 可视区域数据
  const start = visibleInfo.startIndex - aboveCount.value
  const end = visibleInfo.endIndex + belowCount.value
  return listData.value.slice(start, end)
})
const visibleCount = computed(() => { // 可视区域数据个数，向上取整防止留白
  return Math.ceil(visibleInfo.height / itemHeight.value)
})
const phantomHeight = ref(0) // 虚拟列表高度
const containRef = ref(null)
const startOffset = ref(0)
const getTransform = computed(() => { // 偏移值：父相子绝，父有滚动条时，子会随着滚动条滚动，即使有top值
  return `translate3d(0, ${startOffset.value}px, 0)`
})
onMounted(() => {
  visibleInfo.height = containRef.value.clientHeight
  phantomHeight.value = listData.value.length * itemHeight.value
  visibleInfo.startIndex = 0
  visibleInfo.endIndex = visibleInfo.startIndex + visibleCount.value
})
const scrollEvent = (e) => {
  const scrollTop = e.target.scrollTop
  visibleInfo.startIndex = Math.floor(scrollTop / itemHeight.value)
  visibleInfo.endIndex = visibleInfo.startIndex + visibleCount.value
  let scrollCount = visibleInfo.startIndex - aboveCount.value
  if (scrollCount) {
    startOffset.value = scrollCount * itemHeight.value
  } else {
    startOffset.value = 0
  }
}
</script>
<style lang="less">
.container {
  width: 200px;
  height: 300px;
  // -webkit-overflow-scrolling: touch;
  overflow: auto;
  position: relative;
  background-color: aqua;

  .content {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    background-color: yellow;

    .content-item {
      box-sizing: border-box;
      border: 1px solid #ddd;
      text-align: center;
      color: #333;
    }
  }
}
</style>